package de.uni.muenster.idw2.location.bluetooth.service;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.locks.LockSupport;

import de.uni.muenster.idw2.location.bluetooth.service.MeasurementsThread.IFakeReceiver;
import android.app.Service;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.hardware.SensorManager;
import android.os.Binder;
import android.os.Build;
import android.os.Environment;
import android.os.IBinder;
import android.util.Log;

public class PositioningService extends Service implements PositioningServiceInterface {

	//for working with the services 
	SensorManager sensor;
	//category to filter the broadcast intents to get the position.
	public static String BLUETOOH_POSITIONING_POSITION = "BLUETOOTH_POSITION";
	
	//thread in charge of the calculation of the positions
	PositionThread positionThread = null;

	//receiver of the notifications
	AbstractReceiver receiver;

	private static List<BluetoothDeviceData> allDiscoveriesBTDData;
    
	boolean measure = true;
	int searchFinishedCount = 0;
	
	private int currentFloor = 0;
	private boolean saveMeasurements = false;
	
 	//for saving to file is used this as the file name
 	String startTime = "" + System.currentTimeMillis();
	
 	private PositioningMethod positioningMethod = PositioningMethod.pmGeometric;
 	
 	private FloorEstimator floorEstimator = new FloorEstimator();
 	
	private IBinder binder = new PositioningBinder();
	
	public class PositioningBinder extends Binder {
		 public PositioningServiceInterface getService() {
	            // Return this instance of LocalService so clients can call public methods
	            return PositioningService.this;
	        }
	    }

	public PositioningService(){
		super();
	    Log.e("Creating the service", "building");
	}
	
	@Override
	public void onCreate(){
		allDiscoveriesBTDData = Collections.synchronizedList((List<BluetoothDeviceData>)new ArrayList<BluetoothDeviceData>());		
		positionThread = new PositionThread(this, allDiscoveriesBTDData, PositioningMethod.pmGeometric);

		String filename = Environment.getExternalStorageDirectory() + "/allHosts.txt";
		try {
			DataIO.readAllHosts();
			//TODO: Do something better this exception should not just be swallowed here
		} catch (IOException e) {
			e.printStackTrace();
		}
		
		boolean fake = false || isEmulator() ;
		createAndRegisterReceiver(fake);
	    super.onCreate();  
	}
	
	private boolean isEmulator(){
	  return Build.MANUFACTURER.equals("unknown");
	}	
		    
	@Override
	public IBinder onBind(Intent intent) {
		return binder;
	}

	private void createAndRegisterReceiver(boolean fake) {
		String measurementsDirectory = Environment.getExternalStorageDirectory().toString();//+ "/BTTracking/";	
		if (receiver!= null) {
			receiver.unregisterReciever();
		}
		if (!fake) {	
		   receiver = new RealBroadcastReceiver();
		}
		else {
		   receiver = new FakeBroadcastReceiver(measurementsDirectory, false);
		}
		receiver.registerReceiver();
	}

	private void updateAllDiscoveriesData(BluetoothDeviceData lastDiscoveryBTDData){
		 //rules for adding elements.. 
		  synchronized(allDiscoveriesBTDData) {
			 //floor restrictions according to the method
			  if (positioningMethod == PositioningMethod.pmGeometric) {
				  if (lastDiscoveryBTDData.getFloor() == currentFloor) {
					  allDiscoveriesBTDData.add(lastDiscoveryBTDData);
				  }
			  }
			  else {
				  allDiscoveriesBTDData.add(lastDiscoveryBTDData);
			  }
			  
			  if (allDiscoveriesBTDData.size() > 5) {
				  //this only happens if it have 6
				  allDiscoveriesBTDData.remove(0); 
			  }
		  }

		  floorEstimator.addMeasurement(lastDiscoveryBTDData);
		  setCurrentFloor(floorEstimator.getEstimatedFloor());
		  LockSupport.unpark(positionThread);
	}
	
	private void logAllDiscoveredData(){
		String output="";
		synchronized(allDiscoveriesBTDData){
			for(int index=0;index<allDiscoveriesBTDData.size();index++){
				output+=allDiscoveriesBTDData.get(index).getId()+"("+allDiscoveriesBTDData.get(index).getDb_value()+") -";
			}
			Log.i("AMK", "BT-devices found: "+output); 
		}
	}
	
	public static int getDiscoveredDataSize(){
		return allDiscoveriesBTDData.size();
	}
		
	public abstract class AbstractReceiver {
		
		public void OnReceive(String action, Context context, Intent intent, short RSSI, String address) {
			if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(action)) {
				if (measure) {
					searchFinishedCount += 1;
					BluetoothDeviceData.increaseDiscoveryCount();
					//reset the floor estimator so that the values are taken from the new discovery data
					floorEstimator.reset();
				}
			}
			
			if (BluetoothDevice.ACTION_FOUND.equals(action)){
				int floor = DataIO.getFloorForHost(address);
				if (floor!= Integer.MAX_VALUE) {
					BluetoothDeviceData foundDeviceData  = new BluetoothDeviceData(address, RSSI, System.currentTimeMillis(), floor);
					updateAllDiscoveriesData(foundDeviceData);
					//only save the data if it is not running in fake mode and it is explicitly stated 
					//(i.e. by putting saveMeasurements in true)
					if (saveMeasurements && !(receiver instanceof FakeBroadcastReceiver)) 
					  SaveToDisk.saveDataToDisk(startTime, address, RSSI);
					}
			}			
		}
		
		public abstract void registerReceiver();
		
		public abstract void unregisterReciever();
	}
	
	public class RealBroadcastReceiver extends AbstractReceiver {
		
		BroadcastReceiver broadcastReceiver;
		IntentFilter intentFilter;
		BluetoothAdapter mBluetoothAdapter;
		
		public RealBroadcastReceiver(){
			intentFilter = new IntentFilter(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);
			intentFilter.addAction(BluetoothDevice.ACTION_FOUND);

			mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
			
			broadcastReceiver  =
				new BroadcastReceiver(){
			@Override
			public void onReceive(Context context, Intent intent) {
				String action = intent.getAction();
				short dRSSI = 0;
				String dAddress = "";
				
				if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(action)) {
					OnReceive(action, context, intent, dRSSI, dAddress);
					mBluetoothAdapter.startDiscovery();
				}
				else {
					if (BluetoothDevice.ACTION_FOUND.equals(action)) {
						// Get the BluetoothDevice object from the Intent
						BluetoothDevice device = intent
								.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);

						// read BT-address and RSSI
						dRSSI = intent.getShortExtra(
								BluetoothDevice.EXTRA_RSSI, Short.MIN_VALUE);
						dAddress = device.getAddress();

						OnReceive(action, context, intent, dRSSI, dAddress);
					}
				}
			}
		  };
		  
			mBluetoothAdapter.startDiscovery();
		}
		
		public void registerReceiver(){			
			PositioningService.this.registerReceiver(broadcastReceiver, intentFilter);
		}
		
		public void unregisterReciever(){
			mBluetoothAdapter.cancelDiscovery();
			PositioningService.this.unregisterReceiver(broadcastReceiver);
		}				
	} 
	
	public class FakeBroadcastReceiver extends AbstractReceiver implements IFakeReceiver{
		
		String measurementsDirectory;
		boolean cyclic = false;
		MeasurementsThread measurementsThread;
		
		public FakeBroadcastReceiver(String measurementsDirectory, boolean cyclic){
		  this.measurementsDirectory = measurementsDirectory;	
		  this.cyclic = cyclic;	
		  this.measurementsThread = new MeasurementsThread(this, measurementsDirectory);
		}
		
		public void registerReceiver(){			
		  this.measurementsThread.start();
		}
		
		public void unregisterReciever(){
			this.measurementsThread.suspend();
		}	 
	}
	
	/**
	 * Start positioning computation.
	 */
	public void startPositioning() {
		positionThread.start();
	}
	
	public void reStartPositioning(){
		positionThread.startMeasurement();
	}

	public void stopPositioning() {
		positionThread.stopMeasurement();
	}

	@Override
	public void onDestroy()
	{
	  stopPositioning();
	  super.onDestroy();
	}
	
	@Override
	public void onStart(Intent intent, int startId) {
	  super.onStart(intent, startId);
	}
	
	//For telling to the service the current floor
	public void setCurrentFloor(int floor){
		currentFloor = floor;
		positionThread.setCurrentFloor(currentFloor);
	}
	
    //For telling to the service that should save the measurements to files.	
	public void setSaveMeasurements(boolean save){
		saveMeasurements = save;
		//reset the saving time.. initially Andre just destroyed and recreated.
		if (save) {
		   startTime = "" + System.currentTimeMillis();
		}
	}	
    
	//For telling to the service that should use a real or a fake signal strengths generator.
    public void setMeasurementMode(boolean fake) {    	
    	createAndRegisterReceiver(fake);
    }

	public void setPositioningMethod(PositioningMethod positioningMethod) {
		this.positioningMethod = positioningMethod;
	}

	public PositioningMethod getPositioningMethod() {
		return positioningMethod;
	}

}